



#pragma once


#include <initializer_list>

#include "ioxx_include_device.hpp"

#include "ioxx_common.hpp"


namespace adxx {
    enum class sample_speed : uint32_t {
        low = ADC_SampleTime_239_5Cycles,
        middle = ADC_SampleTime_55_5Cycles,
        high = ADC_SampleTime_13_5Cycles,
    };


    enum class channel : uint32_t {
        ain0 = ADC_Channel_0,
        ain1 = ADC_Channel_1,
        ain2 = ADC_Channel_2,
        ain3 = ADC_Channel_3,
        ain4 = ADC_Channel_4,

        ain5_internal_ref = ADC_Channel_5,
    };


    /**
     * @brief 可用的ADC 外设寄存器地址
     *
     */
    enum class adx : uint32_t {
        adc1 = ADC1_BASE,
    };


    enum class adc_mode {
        single_shot,    // 单次模式，扫描一个通道后停止
        discontinuous,  // 不连续模式，扫描序列中一个通道后停止，再次启动时转换下一个通道
    };

    /**
     * @brief 序列扫描方向
     *
     */
    enum class sequence_direction {
        upward = ADC_ScanDirection_Upward,      // 通道编号从小到大
        backward = ADC_ScanDirection_Backward,  // 从大到小
    };


    /**
     * @brief ADC 操作工具对象，只支持单个通道单次转换，或多个通道不连续转换
     *
     * 为了简化和通用，只支持单个通道单次转换，或多个通道不连续转换，不然就要涉及到中断和DMA，限制比较多
     *
     */
    template <adx AdReg = adx::adc1>
    class Adc {
       public:
        constexpr static uint8_t RESOLUTION = 12;                      // 转换位数
        constexpr static uint32_t FULL_SCALE = (1 << RESOLUTION) - 1;  // 采样结果最大值，2^12 - 1 = 4095

       private:
        // ADC1 是ADC1_BASE 经过reinterpret_cast 转换的指针，不能用于constexpr
        // HK32F030 只有ADC1 一个ADC
        constexpr static uint32_t ADC_REG_ADDR = ioxx::_enum_to_underlying(AdReg);

        // CR 寄存器标志位
        constexpr static auto ADEN_BIT = static_cast<uint32_t>(ADC_CR_ADEN);
        constexpr static auto ADSTART_BIT = static_cast<uint32_t>(ADC_CR_ADSTART);
        constexpr static auto ADSTP_BIT = static_cast<uint32_t>(ADC_CR_ADSTP);
        constexpr static auto ADDIS_BIT = static_cast<uint32_t>(ADC_CR_ADDIS);

        // ISR 寄存器标志位
        constexpr static auto ADRDY_FLAG = static_cast<uint32_t>(ADC_ISR_ADRDY);
        constexpr static auto EOC_FLAG = static_cast<uint32_t>(ADC_ISR_EOC);
        constexpr static auto EOS_FLAG = static_cast<uint32_t>(ADC_ISR_EOS);

        // 内部参考电压校准值存储位置首地址，一共四个字节，放在0x1FFF'F834 到 0x1FFF'F837
        constexpr static uint32_t V_REF_INT_CAL_ADDR = 0x1FFF'F834;

        // 将AD 结果转换为实际电压时要乘的系数
        uint32_t _ad_factor = 0;

        // 指示使能的AD 序列中将要转换的是第几个通道，每次EOC 后加一，EOS 后归零
        // 方便使用者追踪转换状态
        uint_fast8_t _sequence_index = 0;

        // 指示序列已转换完成。每次EOS 时置位，不自动清零，通过sequence_completed 函数查询时清零
        bool _sequence_completed = false;

        /**
         * @brief 读取内部参考电压校准值
         *
         * 这是在生产中写入芯片的值，代表VDDA 电压为3.3V 时，ADC 测量内部0.8V 参考电压的结果
         *
         * @return uint32_t
         */
        uint32_t _read_v_ref_int_cal() const {
            return *(reinterpret_cast<const uint32_t*>(V_REF_INT_CAL_ADDR));
        }

       public:
        /**
         * @brief 指向ADC 外设寄存器的指针
         *
         * @return ADC_TypeDef*
         */
        ADC_TypeDef* reg() const {
            return reinterpret_cast<ADC_TypeDef*>(ADC_REG_ADDR);
        }


        void enable_clk() const {
            // 开启时钟
            RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC, ENABLE);
            RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);  // PWR 模块控制内部参考电压
        }


        /**
         * @brief 终止AD 转换，然后可以修改配置寄存器
         *
         */
        void stop() {
            // 如果转换正在运行，则置位ADSTP 停止转换
            if (reg()->CR & ADSTART_BIT) {
                reg()->CR |= ADSTP_BIT;

                // 等待ADSTP 变为0，表示ADC 已停止
                while (reg()->CR & ADSTP_BIT);
            }
        }


        /**
         * @brief 关闭ADC, ADEN = 0
         *
         */
        void disable() {
            if (reg()->CR & ADEN_BIT) {
                reg()->CR |= ADDIS_BIT;

                // 等待ADDIS 位被硬件清零，表示ADC 已关闭
                while (reg()->CR & ADDIS_BIT);
            }
        }


        /**
         * @brief 设置ADEN = 1
         *
         */
        void enable() {
            reg()->CR |= ADEN_BIT;

            // 等待，直到ADC 准备好
            // TODO: 超时控制
            while (!(reg()->ISR & ADRDY_FLAG));
        }


        auto is_enabled() {
            return reg()->CR & ADEN_BIT;
        }


        /**
         * @brief 关闭ADC，复位时钟
         *
         * 使ADC 回到复位状态，ADEN == 0，此时才能校准
         *
         */
        void reset() {
            stop();
            disable();
            ADC_DeInit(ADC1);
        }


        /**
         * @brief 校准ADC 内部参数，必须在ADEN == 0 时执行
         *
         */
        void calibrate() {
            // if(is_enabled()) {
            //     reset();
            // }
            // TODO: 超时控制
            ADC_GetCalibrationFactor(reg());
        }


        /**
         * @brief 设置采样速度，默认最低速
         *
         * 信号源内阻越大，ADC 采样电容就需要越长的的充电时间，默认最低速，兼容性最好。
         *
         * @param ss
         */
        void set_sample_speed(sample_speed ss = sample_speed::low) {
            reg()->SMPR = ioxx::_enum_to_underlying(ss);
        }


        /**
         * @brief 设置输入通道
         *
         * 使用不连续模式时，可以一次设置多个通道，单次模式只允许设置一个通道
         *
         * @param channel_list
         */
        void set_channel(std::initializer_list<channel> channel_list) {
            uint32_t sum = 0;
            for (auto c : channel_list) {
                sum |= static_cast<uint32_t>(c);
            }

            reg()->CHSELR = static_cast<uint32_t>(sum);
        }

        void set_channel(channel c) {
            reg()->CHSELR = static_cast<uint32_t>(c);
        }


        /**
         * @brief 初始化ADC 工作模式，之后还需要调用enable 使能ADC
         *
         * 可以选择使用单次模式或不连续模式，不连续模式可以选择多个转换通道，单次模式如果选择了多通道，会发生数据错误。
         *
         * 默认扫描方向为向下扫描，这是为了兼容PY32F030 的ADC 的一个硬件问题：如果序列中只有一个通道，必须选择向下扫描。
         *
         * @param m    ADC 模式
         * @param dir  扫描方向
         */
        void init(adc_mode m, sequence_direction dir = sequence_direction::backward) {
            enable_clk();
            reset();

            ADC_InitTypeDef ADC_InitStructure;

            // ADC 默认模式：单次转换序列中所有通道，序列转换完成，触发EOC 和EOS 后，自动停止, ADSTART = 0
            // 因为是单通道单次转换，所以转换序列将只有一个通道，EOC 和EOS 同时发出
            ADC_StructInit(&ADC_InitStructure);
            ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;

            ADC_InitStructure.ADC_ExternalTrigConvEdge = ADC_ExternalTrigConvEdge_None;
            ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
            ADC_InitStructure.ADC_ScanDirection = static_cast<uint8_t>(dir);
            ADC_Init(ADC1, &ADC_InitStructure);

            // ADC 采用独立14MHz 时钟驱动
            // TODO：不同主频的芯片配置都不同
            ADC_ClockModeConfig(ADC1, ADC_ClockMode_AsynClk);

            // 配置为Overrun 模式，DR 数据没被读取时也可以开启转换
            ADC_OverrunModeCmd(ADC1, ENABLE);

            if (m == adc_mode::discontinuous) {
                ADC_DiscModeCmd(ADC1, ENABLE);
            }
            else {
                ADC_DiscModeCmd(ADC1, DISABLE);
            }

            // 启动并设置内部参考为0.8V
            PWR_SetLDO_RefVolToADC(ADC_VREF_0D8);

            // 启用内部参考通道
            ADC_VrefintCmd(ADC1, ENABLE);

            // ADC 自校准
            calibrate();

            // 默认最低采样速度
            set_sample_speed(sample_speed::low);
        }


        /**
         * @brief 根据已知ADC 满幅电压，计算AD 系数
         *
         * 如果ADC 以VDDA 为参考，且VDDA 为精确值，则AD 系数 = VDDA / FULL_SCALE
         * 如果连接了外部基准REF ，AD 系数 = REF / FULL_SCALE
         *
         * @param full_scale_ref   单位微伏
         * @return uint32_t
         */
        uint32_t ad_factor_by_full_scale(uint32_t full_scale_ref) {
            _ad_factor = full_scale_ref / FULL_SCALE;
            return _ad_factor;
        }


        /**
         * @brief 根据参考电压实测值，计算AD 系数
         *
         * 计算结果会保留在对象内部，供real_value() 函数使用。
         *
         * @param ref         内部或外部参考电压，单位微伏
         * @param ad_result   测量参考电压所得的ADC 转换值
         *
         * @return uint32_t 单位是微伏
         */
        uint32_t ad_factor_by_ref(uint32_t ref, uint32_t ad_result) {
            _ad_factor = ref / ad_result;
            return _ad_factor;
        }


        /**
         * @brief 根据ADC 转换结果计算实际电压
         *
         * 必须先通过ad_factor 函数设置转换系数
         *
         * @return uint32_t
         */
        uint32_t real_value(uint16_t adc_result) const {
            return _ad_factor * adc_result;
        }


        /**
         * @brief 检查序列是否至少转换完成一次
         *
         * EOS 后置位，不自动清零，调用函数后返回标志并清零。
         *
         * @return true
         * @return false
         */
        bool sequence_completed() {
            bool v = _sequence_completed;
            _sequence_completed = false;
            return v;
        }


        /**
         * @brief 开始转换，或在不连续模式触发下一个通道的转换
         *
         * @return uint_fast8_t 将要或正在转换的通道次序
         */
        uint_fast8_t start() {
            // 如果转换已开始，不再次开始转换
            // 不连续模式下，触发EOC 时ADSTART 不会复位，所以EOC 置位时也可以开始
            if (!(reg()->CR & ADSTART_BIT) || reg()->ISR & EOC_FLAG) {
                // 先要使能ADC
                if(!is_enabled()) {
                    enable();
                }

                reg()->ISR = EOC_FLAG;  // 清零标志
                reg()->CR |= ADSTART_BIT;

                // TODO: 对PY32F030 输出warning，提醒要在en 后一段时间再start，
                // 所以start 函数可能会内部死循环延时一小段时间
            }

            return _sequence_index;
        }


        /**
         * @brief 检查一次转换是否已完成
         *
         * 单次转换模式下，一次转换结束会同时触发序列转换结束，有可能需要复位ADC（PY32F0）。
         * 不连续转换模式下，下次start 会转换序列中下一个通道。
         *
         * 返回的通道次序取决于通道配置和转换反向，通道次序本身只是个转换计数器。
         * 序列结束，EOS 后次序复位，重新开始计数
         *
         * @return int_fast8_t 完成转换的通道的次序，未完成时返回-1
         */
        int_fast8_t completed() {
            if (reg()->ISR & EOC_FLAG) {
                // 如果EOS 置位，表示序列转换结束，索引清零重新开始
                uint_fast8_t v = _sequence_index;

                if (reg()->ISR & EOS_FLAG) {
                    _sequence_completed = true;
                    reg()->ISR = EOS_FLAG;
                    
                    _sequence_index = 0;
                }
                else {
                    ++_sequence_index;
                }
                // EOC 标志在start 函数中清零
                
                return static_cast<int_fast8_t>(v);
            }
            else {
                return -1;
            }
        }


        /**
         * @brief  返回原始转换结果，默认12bit 右对齐
         *
         * @return int16_t
         */
        uint16_t fetch() const {
            return reg()->DR;
        }


        /**
         * @brief 通过ad_factor 函数设置了转换系数后，可以直接获取转换后的实际电压值
         *
         * @return uint32_t  实际电压值，单位微伏
         */
        uint32_t fetch_real() const {
            return real_value(fetch());
        }
    };

}  // namespace adxx